const ffi = require('@lwahonen/ffi-napi');
const ref = require('@lwahonen/ref-napi');
const iconv = require('iconv-lite');
const Struct = require('ref-struct-di')(ref);
const path = require('path');
const { execSync } = require('child_process');


// 定义 Windows 类型
const voidPtr = ref.refType(ref.types.void); // 等效于 void*
const BOOL = ref.types.bool;
const DWORD = ref.types.uint32;
const HANDLE = voidPtr;
const HICON = voidPtr; // 图标句柄类型声明


const STARTUPINFO = Struct({
    cb: 'uint',
    lpReserved: 'pointer',
    lpDesktop: 'string',
    lpTitle: 'string',
    dwX: 'uint',
    dwY: 'uint',
    dwXSize: 'uint',
    dwYSize: 'uint',
    dwXCountChars: 'uint',
    dwYCountChars: 'uint',
    dwFillAttribute: 'uint',
    dwFlags: 'uint',
    wShowWindow: 'ushort',
    cbReserved2: 'ushort',
    lpReserved2: 'pointer',
    hStdInput: 'pointer',
    hStdOutput: 'pointer',
    hStdError: 'pointer'
})
const MIIM_STRING = 0x00000040;
const MIIM_SUBMENU = 0x00000004;
const MIIM_ID = 0x00000002; // 新增：用于读取 wID

const PROCESS_INFORMATION = Struct({
    hProcess: HANDLE,
    hThread: HANDLE,
    dwProcessId: DWORD,
    dwThreadId: DWORD
})




let voidPointerType = ref.refType(ref.types.void);
let cStringPointerType = ref.refType(ref.types.CString);
const LONG_PTR = ref.types.int64; // 64 系统用 int64，32 位系统用 int32
// 定义常量
const GWL_STYLE = -16;
const WS_MINIMIZEBOX = 0x00020000;

const user32 = ffi.Library('user32.dll', {
    'EnumWindows': ['int', [voidPointerType, 'int32']],
    'EnumChildWindows': ['int', ['int32', voidPointerType, 'int32']],
    'GetClassNameA': ['int', ['int', 'string', 'int']],
    'GetWindowTextA': ['long', ['long', cStringPointerType, 'long']],
    'GetWindowThreadProcessId': ['int', ['int', 'pointer']],
    'WaitForInputIdle': ['uint', ['pointer', 'uint']],
    // 'SendMessageA': ['bool', ['long', 'long', 'long', cStringPointerType]],
    'SendMessageA': ['int', ['int', 'uint32', 'int', 'pointer']],
    'PostMessageA': ['int', ['int', 'uint32', 'int', 'int']],
    'PostMessageW': ['bool', ['long', 'long', 'long', 'long']],
    'GetMenu': ['int', ['int']],
    'GetMenuItemCount': ['int', ['int']],
    'GetMenuStringA': ['int', ['int', 'int', cStringPointerType, 'int', 'int']],
    'GetSubMenu': ['int', ['int', 'int']],
    GetMenuItemInfoW: ['bool', ['int', 'int', 'bool', 'pointer']],
    IsWindowVisible: ['bool', ['int']],
    GetParent: ['int', ['int']],
    GetWindowLongPtrW: [LONG_PTR, ['int', 'int']],
    DestroyIcon: ['bool', [HICON]],
    ShowWindow: ['bool', ['int', 'int']],
    GetWindow: ['int', ['int', 'int']],
    GetParent: ['int', ['int']],
    GetAncestor: ['int', ['int', 'int']],
    GetTopWindow: ['int', ['int']],
    SetWindowTextW: ["bool", ["int", "string"]]
})


const kernel32 = ffi.Library('kernel32.dll', {
    CreateProcessA: [
        BOOL, [
            ref.types.CString, // lpApplicationName
            ref.types.CString, // lpCommandLine
            'pointer', // lpProcessAttributes
            'pointer', // lpThreadAttributes
            'bool', // bInheritHandles
            'uint', // dwCreationFlags
            'pointer', // lpEnvironment
            'string', // lpCurrentDirectory
            "pointer", // lpStartupInfo
            "pointer" // lpProcessInformation
        ]
    ],
    GetLastError: ['uint32', []],
    OpenProcess: ['pointer', ['uint', 'bool', 'uint']],
    CloseHandle: ['bool', ['pointer']],
    WaitForSingleObject: ['uint', ['pointer', 'uint']],
    TerminateProcess: ['bool', ['uint', 'uint']]
});

// 绑定 API
const shell32 = new ffi.Library('shell32', {
    ExtractIconExW: ['uint', [ref.types.CString, 'int', HICON, HICON, 'uint']]
});

const psapi = new ffi.Library('psapi', {
    GetModuleFileNameExW: ['uint', ['uint', 'uint', cStringPointerType, 'uint']],
    GetModuleBaseNameW: ['uint', ['uint', 'uint', cStringPointerType, 'uint']]
});


// 获取图标函数
function getAppIcon(exePath) {
    const largeIcons = Buffer.alloc(ref.sizeof.pointer);
    const smallIcons = Buffer.alloc(ref.sizeof.pointer);

    // 提取第一个图标
    const count = shell32.ExtractIconExW(
        ref.allocCString(exePath),
        -1, // 提取所有图标
        largeIcons,
        smallIcons,
        1
    );

    if (count <= 0) throw new Error('No icons found');
    return hIconLarge.deref()
    // // 转换为图标文件并保存到临时目录
    // const tempPath = path.join(os.tmpdir(), 'app_icon.ico');
    // saveHIconToFile(hIconLarge.deref(), tempPath); // 需自定义实现
    // return tempPath;
}

// 判断窗口是否有最小化按钮
function hasMinimizeButton(hwnd) {
    // 获取窗口样式
    const style = user32.GetWindowLongPtrW(hwnd, GWL_STYLE);
    // 检查是否包含 WS_MINIMIZEBOX 标志
    return (style & WS_MINIMIZEBOX) !== 0;
}

/**
 * 
 * @param {number} p1 
 * @returns {string}
 */
function getWindowTextA(p1) {
    const alloc = Buffer.alloc(255);
    user32.GetWindowTextA(p1, alloc, 255);
    const from = Buffer.from(iconv.decode(alloc, 'gbk'));
    const readCString = ref.readCString(from, 0);
    return readCString;
};

function setWindowTextW(hwnd, title) {
    // 修改: 使用 Buffer 将字符串转换为 UTF-16 编码
    const titleBuffer = Buffer.from(title + '\0', 'ucs2');
    return user32.SetWindowTextW(hwnd, titleBuffer)
}
/**
 * 
 * @param {number} p1 
 * @returns {string}
 */
function getClassNameA(p1) {
    const alloc = Buffer.alloc(255);
    user32.GetClassNameA(p1, alloc, 255);
    const from = Buffer.from(iconv.decode(alloc, 'gbk'));
    const readCString = ref.readCString(from, 0);
    return readCString;
};

/**
 * 
 * @param {number} p1 
 * @returns {number}
 */
function getWindowThreadProcessId(p1) {
    const alloc = Buffer.alloc(8);
    user32.GetWindowThreadProcessId(p1, alloc);
    const readUint32LE = alloc.readUint32LE(0);
    return readUint32LE;
};

/**
 * 
 * @param {number} p1 
 * @returns {string}
 */
function getMessage(p1) {
    let alloc = Buffer.alloc(1048576);
    user32.SendMessageA(p1, 13, alloc.length, alloc);
    let from = Buffer.from(iconv.decode(alloc, 'gbk'));
    let readCString = ref.readCString(from, 0);
    return readCString;
}
function GetMenuStringA(p1, p2) {
    let alloc = Buffer.alloc(120);
    user32.GetMenuStringA(p1, p2, alloc, 120, 0);
    let from = Buffer.from(iconv.decode(alloc, 'gbk'));
    let readCString = ref.readCString(from, 0);
    return readCString;
};

/**
 * 
 * @param {string} filename 
 * @param {{workingDirectory?:string|boolean, delay:number, wShowWindow:number}} options
 * @returns {Promise<number>} 
 */
function createProcessA(filename, options = {}) {
    return new Promise(async (resolve, reject) => {
        const startupInfo = new STARTUPINFO();
        if (options.wShowWindow !== undefined) {
            startupInfo.dwFlags = 0x00000001
            startupInfo.wShowWindow = options.wShowWindow
        }

        startupInfo.cb = STARTUPINFO.size;
        const processInfo = new PROCESS_INFORMATION();

        let workingDirectory = ""
        if (typeof options.workingDirectory === 'string') {
            workingDirectory = options.workingDirectory
        } else if (options.workingDirectory !== false) {
            workingDirectory = path.dirname(filename);
        }
        const success = kernel32.CreateProcessA(
            null,               // 应用程序名（用命令行替代）
            filename,           // 完整命令行
            null,               // 进程安全属性
            null,               // 线程安全属性
            false,              // 不继承句柄
            0,                  // 无特殊标志
            null,               // 使用父进程环境
            workingDirectory,   // 使用父进程目录
            startupInfo.ref(),
            processInfo.ref()
        );

        if (!success) {
            const errorCode = kernel32.GetLastError();
            reject(new Error(`启动失败:${filename} (错误码: ${errorCode})`))
            return
        }
        const waitResult = user32.WaitForInputIdle(processInfo.hProcess, 10000);
        if (waitResult === 0xFFFFFFFF) {
            reject(new Error(`启动超时:${filename}`))
            return;
        }
        await new Promise((resolve) => setTimeout(resolve, options.delay || 0));
        resolve(processInfo.dwProcessId);
    });
}

/**
 * 
 * @param {string} filename 
 * @param {string} workingDirectory
 * @param {number} delay 
 * @returns {Promise<number>}
 */
function launchApplication(filename, workingDirectory = null, delay = 0) {
    if (!workingDirectory) {
        workingDirectory = path.dirname(filename);
    }
    return new Promise(async (resolve, reject) => {
        const startupInfo = new STARTUPINFO();
        if (path.basename(filename).toLowerCase() !== 'm2server.exe') {
            // MirServer.ese最小化后再打开，不能再最小化了，先这样临时解决，实在不行，单独用showWindow来操作
            startupInfo.dwFlags = 0x00000001
            startupInfo.wShowWindow = 7
        }
        startupInfo.cb = STARTUPINFO.size;
        const processInfo = new PROCESS_INFORMATION();
        const success = kernel32.CreateProcessA(
            null,               // 应用程序名（用命令行替代）
            filename,           // 完整命令行
            null,               // 进程安全属性
            null,               // 线程安全属性
            false,              // 不继承句柄
            0,                  // 无特殊标志
            null,               // 使用父进程环境
            workingDirectory,   // 使用父进程目录
            startupInfo.ref(),
            processInfo.ref()
        );

        if (!success) {
            const errorCode = kernel32.GetLastError();
            reject(new Error(`启动失败:${filename} (错误码: ${errorCode})`))
            return
        }
        const waitResult = user32.WaitForInputIdle(processInfo.hProcess, 10000);
        if (waitResult === 0xFFFFFFFF) {
            reject(new Error(`启动超时:${filename}`))
            return;
        }
        await new Promise((resolve) => setTimeout(resolve, delay));
        // user32.PostMessageA(windows[index].hwnd, 0x0112, 0xF020, 0); // WM_SYSCOMMAND, SC_MINIMIZE
        resolve(processInfo.dwProcessId);
    });
}

/**
 * 
 * @param {number} processID 
 * @returns {Promise<{handle:number,title:string,class:string,processID:number}|null>}
 */
function getMainHandleByProcess(processID, debug = false) {
    return new Promise((resolve) => {
        user32.EnumWindows(ffi.Callback('int', ['int32', 'int32'], function (p1, p2) {
            const ClassName = getClassNameA(p1);
            const WindowThreadPId = getWindowThreadProcessId(p1);
            if (WindowThreadPId === processID && ["tfrmmain", "tfrmlogdata"].includes(ClassName.toLowerCase())) {
                const WindowText = getWindowTextA(p1);
                console.log("===", ClassName, p1, WindowText);
                resolve({ handle: p1, title: WindowText, class: ClassName, processID: WindowThreadPId });
            } else if (debug) {
                const WindowText = getWindowTextA(p1);
                console.log("---", ClassName, p1, WindowText, WindowThreadPId === processID);
            }

            return 1;
        }), 0);
        resolve(null);
    })
}



function findPortsByProcessId(processId) {
    const command = `chcp 65001 > nul && netstat -ano | findstr "${processId}"`;
    const output = execSync(`${command} 2> nul`, { encoding: 'utf8' })
    return output.match(/(?<=(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}:))(\d+)/g)?.filter(v => v > 0).map(v => parseInt(v))
}

function findProcessesByFilename(filename) {
    const command = `chcp 65001 > nul && wmic process where name="${path.basename(filename)}" get processid,executablepath,name /format:csv`;
    const output = execSync(`${command} 2> nul`, { encoding: 'utf-8' })
    const lines = output.split("\r\n").filter(line => !!line.trim())
    const keys = lines[0].split(",").map(key => key.trim()).filter(v => !!v)
    const list = []
    for (let index = 0; index < lines.length; index++) {
        const line = lines[index].trim();
        if (index === 0 || !line) {
            continue
        }
        const values = line.split(",").map(value => value.trim())
        /** @type {{Node:string,ProcessId:string,ExecutablePath:string,Name:string}} */
        const obj = {}
        keys.forEach((key, index) => obj[key] = values[index])
        if (obj.ExecutablePath !== filename) {
            continue
        }
        list.push(obj)
    }
    return list
}

/**
 * @typedef {{ className:string, processId:number, text:string, hwnd:number, lParam: any }} WindowInfo
 */

/**
 * 
 * @param {(info:WindowInfo) =>WindowInfo|boolean } callback 
 * @param {number} lParam 
 * @returns {Promise<WindowInfo[]>}
 */
function findWindows(callback, lParam = 0) {
    return new Promise((resolve) => {
        const list = []
        user32.EnumWindows(ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
            const ClassName = getClassNameA(hwnd);
            const processID = getWindowThreadProcessId(hwnd);
            const text = getWindowTextA(hwnd);
            const info = { ClassName, processID, text, hwnd, lParam: lp }
            const result = callback(info)
            if (result === true) {
                list.push(info)
                resolve(list)
            } else if (result) {
                list.push(info)
            }
            return 1;
        }), lParam);
        resolve(list);
    })
}
/**
 * @template WT
 * @param {(handle:WindowHandle) =>WT|boolean } callback 
 * @param {number} lParam 
 * @returns {Promise<WT[]>}
 */
function enumWindows(callback, lParam = 0) {
    return new Promise((resolve) => {
        const list = []
        user32.EnumWindows(ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
            const className = getClassNameA(hwnd);
            const processId = getWindowThreadProcessId(hwnd);
            const text = getWindowTextA(hwnd);
            const handle = { className, processId, text, hwnd, lParam: lp }
            const result = callback(handle)
            if (result === true) {
                list.push(handle)
                resolve(list)
            } else if (result) {
                list.push(result)
            }
            return 1;
        }), lParam);
        resolve(list);
    })
}

/**
 * @template WT
 * @param {number} pHwnd 
 * @param {(info:WindowInfo) =>WT|boolean } callback 
 * @param {number} lParam 
 * @returns {Promise<WT[]>}
 * 
 */
function findChildWindows(pHwnd, callback, lParam = 0) {
    return new Promise((resolve) => {
        const list = []
        user32.EnumChildWindows(pHwnd, ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
            const ClassName = getClassNameA(hwnd);
            const processID = getWindowThreadProcessId(hwnd);
            const text = getWindowTextA(hwnd);
            const info = { ClassName, processID, text, hwnd, lParam: lp }
            const result = callback(info)
            if (result === true) {
                list.push(info)
                resolve(list)
            } else if (result) {
                list.push(info)
            }
            return 1;
        }), lParam);
        resolve(list);
    })
}
/**
 * 
 * @param {number} pHwnd 
 * @param {(handle:WindowHandle) =>WindowHandle|boolean } callback 
 * @param {number} lParam 
 * @returns {Promise<WindowHandle[]>}
 */
function enumChildWindows(pHwnd, callback, lParam = 0) {
    return new Promise((resolve) => {
        const list = []
        user32.EnumChildWindows(pHwnd, ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
            const className = getClassNameA(hwnd);
            const processId = getWindowThreadProcessId(hwnd);
            const text = getWindowTextA(hwnd);
            const handle = { className, processId, text, hwnd }
            const result = callback(handle)
            if (result === true) {
                list.push(handle)
                resolve(list)
            } else if (result) {
                list.push(handle)
            }
            return 1;
        }), lParam);
        resolve(list);
    })
}


/**
 * 
 * @param {number} hwnd 
 * @returns {WindowHandle}
 */
function getWindowInfo(hwnd) {
    const className = getClassNameA(hwnd);
    const processId = getWindowThreadProcessId(hwnd);
    const text = getWindowTextA(hwnd);
    return { className, processId, text, hwnd }
}
/**
 * 
 * @param {(info:WindowHandle)=>boolean} callback 
 * @param {number} lParam 
 * @returns {Promise<WindowHandle>}
 */
function findWindow(callback, lParam = 0) {
    return new Promise((resolve, reject) => {
        user32.EnumWindows(ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
            const info = getWindowInfo(hwnd)
            const result = callback(info)
            if (result) {
                resolve(info)
                return 0;
            }
            return 1;
        }), lParam);
        resolve(null)
    })
}
/**
 * 
 * @param {(info:WindowHandle)=>boolean} callback 
 * @param {number} lParam 
 * @returns {Promise<WindowHandle[]>}
 */
function filterWindow(callback, lParam = 0) {
    const list = []
    return new Promise((resolve, reject) => {
        user32.EnumWindows(ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
            const info = getWindowInfo(hwnd)
            const result = callback(info)
            if (result) {
                list.push(info)
            }
            return 1;
        }), lParam);
        resolve(list)
    })
}

function findChildWindow(pHwnd, callback, lParam = 0) {
    return new Promise((resolve, reject) => {
        user32.EnumChildWindows(pHwnd, ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
            const info = getWindowInfo(hwnd)
            const result = callback(info)
            if (result) {
                resolve(info)
                return 0;
            }
            return 1;
        }), lParam);
        resolve(null)
    })
}

function enumChildWindowsEx(pHwnd, callback, lParam = 0) {
    return user32.EnumChildWindows(pHwnd, ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
        const className = getClassNameA(hwnd);
        const processId = getWindowThreadProcessId(hwnd);
        const text = getWindowTextA(hwnd);
        const handle = { className, processId, text, hwnd }
        return callback(handle) ? 0 : 1;
    }), lParam);
}



/**
 * 
 * @param {number} handle 
 * @returns {Promise<{handle:number,title:string,class:string}|null>}
 */
function getMessageHandleByMain(handle) {
    return new Promise((resolve) => {
        user32.EnumChildWindows(handle, ffi.Callback('int', ['int32', 'int32'], function (hwnd, lp) {
            let ClassName = getClassNameA(p1);
            let classNameslist = [
                'TMemo',
                'TRichEdit',
                'TRichView',
                'TJvRichEdit'
            ];
            if (classNameslist.includes(ClassName)) {
                const WindowText = getWindowTextA(p1);
                resolve({ handle: p1, title: WindowText, class: ClassName });
            }
            return 1;
        }), 0);
        resolve(null);
    })
}

function getMenuInfo(hMenu, index) {
    // 分配 MENUITEMINFOW 结构体内存（64位系统）
    const miiBuffer = Buffer.alloc(80);
    const textBuffer = Buffer.alloc(1024 * 2);

    // 手动设置结构体字段（关键偏移量）：
    miiBuffer.writeUInt32LE(80, 0);           // cbSize = 80
    miiBuffer.writeUInt32LE(MIIM_STRING | MIIM_SUBMENU | MIIM_ID, 4); // fMask
    miiBuffer.writeUInt32LE(0, 56);           // dwTypeData 指针位置
    miiBuffer.writeUInt32LE(1024, 64);        // cch

    // 设置文本缓冲区地址
    const textBufferPtr = ref.address(textBuffer);
    miiBuffer.writeBigUInt64LE(BigInt(textBufferPtr), 56);

    // 调用 GetMenuItemInfoW
    const success = user32.GetMenuItemInfoW(hMenu, index, true, miiBuffer);

    if (success) {
        // 读取 wID（偏移量 16，64位系统）
        const wID = miiBuffer.readUInt32LE(16);
        const text = textBuffer.toString('ucs2').split('\0')[0];
        // 如果有子菜单，递归遍历
        const hSubMenu = miiBuffer.readPointer(20); // hSubMenu 偏移量20
        return { wID, text, hSubMenu };
    }
    return null
}


/**
 * 递归遍历菜单并记录命令 ID
 * @param {number} hMenu 
 * @param {number} level 
 * @param {number} pid 
 * @returns {MenuInfo[]}
 */
function traverseMenu(hMenu, level = 0, pid = 0) {
    const list = [];
    const itemCount = user32.GetMenuItemCount(hMenu);
    if (itemCount === -1) return list;

    for (let i = 0; i < itemCount; i++) {
        const info = getMenuInfo(hMenu, i);
        if (!info) {
            continue
        }
        // console.log(`${'  '.repeat(level)}[${i}] ID=${info.wID}, 文本=${info.text}`);
        const menu = { id: info.wID, text: info.text, level, pid }
        if (!ref.isNull(info.hSubMenu)) {
            const hSubMenuActual = user32.GetSubMenu(hMenu, i);
            menu.children = traverseMenu(hSubMenuActual, level + 1, info.wID)
        }
        list.push(menu)
    }
    return list;
}

/**
 * 
 * @param {MenuInfo[]} menus 
 */
function executeMenuCommand(hwnd, menus) {
    menus.forEach(menu => {
        user32.PostMessageW(hwnd, 273, menu.id, 0)
    });
}


module.exports = {
    user32, kernel32, psapi,
    STARTUPINFO, PROCESS_INFORMATION,
    findWindows, findChildWindows, getMainHandleByProcess, getMessageHandleByMain, getMessage, enumWindows, enumChildWindows,
    GetMenuStringA, getMenuInfo, traverseMenu, executeMenuCommand, findProcessesByFilename, findPortsByProcessId,
    launchApplication, hasMinimizeButton, getAppIcon, createProcessA, setWindowTextW, getWindowTextA, filterWindow, findWindow, enumChildWindowsEx, findChildWindow
}